//
//  CWLocation.m
//  CWDevelop
//
//  Created by LittoCats on 7/22/14.
//  Copyright (c) 2014 Littocats. All rights reserved.
//

#import "CWLocation.h"

#define kDoubleNumber(...)  [NSNumber numberWithDouble:__VA_ARGS__]

@interface CWLocation () <CLLocationManagerDelegate>

@property (nonatomic, strong) NSDictionary *locationInfo;

@property (nonatomic, strong) CLLocationManager* locationManager;

@property (nonatomic, strong) NSMutableArray* updateUserLocationCompletionHandlers;
@end

@implementation CWLocation
+ (instancetype)shareInstance
{
    static CWLocation *location;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        location = [[CWLocation alloc] init];
        location.locationManager = [[CLLocationManager alloc] init];
        //设置代理
        [location.locationManager setDelegate:location];
        //设置精准度，
        [location.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
        
        location.updateUserLocationCompletionHandlers = [NSMutableArray new];
    });
    return location;
}
+ (void)updateUserLocationWithCompletionHandler:(void (^)(NSArray *))handler
{
    CWLocation *location = [CWLocation shareInstance];
    [location.updateUserLocationCompletionHandlers addObject:handler];
    [location.locationManager startUpdatingLocation];
}

#pragma mark- CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager
	 didUpdateLocations:(NSArray *)locations
{
    [manager stopUpdatingLocation];
    while (self.updateUserLocationCompletionHandlers.count) {
        void (^handler)(NSArray *) = [self.updateUserLocationCompletionHandlers firstObject];
        handler(locations);
        [self.updateUserLocationCompletionHandlers removeObjectAtIndex:0];
    }
}

#pragma mark-
#pragma mark- private method
+ (CWLocation *)locationWithLocationInfo:(NSDictionary *)location
{
    CLLocationCoordinate2D coordinate ;
    coordinate.latitude = [location[@"latitude"] doubleValue];
    coordinate.longitude = [location[@"longitude"] doubleValue];
    if ([location[@"coordinateType"] isEqualToString:@"WGS"])
        coordinate = transformFromWGSToGCJ(coordinate);
    else if ([location[@"coordinateType"] isEqualToString:@"BD"])
        coordinate = bd_decrypt(coordinate);

    
    
    CWLocation *result = [[CWLocation alloc] initWithCoordinate:coordinate altitude:[location[@"altitude"] doubleValue] horizontalAccuracy:[location[@"horizontalAccuracy"] doubleValue] verticalAccuracy:[location[@"verticalAccuracy"] doubleValue] course:[location[@"course"] doubleValue] speed:[location[@"speed"] doubleValue] timestamp:[NSDate dateWithTimeIntervalSinceReferenceDate:[location[@"timestamp"] doubleValue]]];
    return result;
}

+ (CWLocation *)locationWithWGSLocation:(CLLocation *)location
{
    return [[CWLocation alloc] initWithCoordinate:transformFromWGSToGCJ(location.coordinate)
                                         altitude:location.altitude
                               horizontalAccuracy:location.horizontalAccuracy
                                 verticalAccuracy:location.verticalAccuracy
                                           course:location.course
                                            speed:location.speed
                                        timestamp:location.timestamp];
}

+ (CWLocation *)locationWithBDLocation:(CLLocation *)location
{
    return [[CWLocation alloc] initWithCoordinate:bd_decrypt(location.coordinate)
                                         altitude:location.altitude
                               horizontalAccuracy:location.horizontalAccuracy
                                 verticalAccuracy:location.verticalAccuracy
                                           course:location.course
                                            speed:location.speed
                                        timestamp:location.timestamp];
}

- (NSDictionary *)locationInfoWithOptions:(CWCoordinateOptions)options
{
    if (!self.locationInfo) {
        CLLocationCoordinate2D coordinate = self.coordinate;
        self.locationInfo = @{@"latitude": kDoubleNumber(coordinate.latitude),
                              @"longitude":kDoubleNumber(coordinate.longitude),
                              @"speed":kDoubleNumber(self.speed),
                              @"course":kDoubleNumber(self.course),
                              @"altitude":kDoubleNumber(self.altitude),
                              @"timestamp":kDoubleNumber([self.timestamp timeIntervalSinceReferenceDate]),
                              @"coordinateType": options == CWCoordinateOptionsBaidu ? @"BD" :
                                  options == CWCoordinateOptionsWGS ? @"WGS" : @"GCJ"};
    }
   CLLocationCoordinate2D coordinate = options == CWCoordinateOptionsWGS ? transformFromGCJToWGS(self.coordinate) : options == CWCoordinateOptionsBaidu ? bd_encrypt(self.coordinate) : self.coordinate;
    NSMutableDictionary *locationInfo = [NSMutableDictionary dictionaryWithDictionary:self.locationInfo];
    [locationInfo setObject:kDoubleNumber(coordinate.latitude) forKey:@"latitude"];
    [locationInfo setObject:kDoubleNumber(coordinate.longitude) forKey:@"longitude"];
    [locationInfo setObject:options == CWCoordinateOptionsBaidu ? @"BD" : options == CWCoordinateOptionsWGS ? @"WGS" : @"GCJ" forKey:@"coordinateType"];
    
    return locationInfo;
}
@end


CLLocationCoordinate2D LocationMake(double lng, double lat) {
    CLLocationCoordinate2D loc;
    loc.longitude = lng, loc.latitude = lat; return loc;
}

///
///  Transform WGS-84 to GCJ-02 (Chinese encrypted coordination system)
///

const double pi = 3.14159265358979324;

//
// Krasovsky 1940
//
// a = 6378245.0, 1/f = 298.3
// b = a * (1 - f)
// ee = (a^2 - b^2) / a^2;
const double a = 6378245.0;
const double ee = 0.00669342162296594323;

int outOfChina(double lat, double lon)
{
    if (lon < 72.004 || lon > 137.8347)
        return 1;
    if (lat < 0.8293 || lat > 55.8271)
        return 1;
    return 0;
}

double transformLat(double x, double y)
{
    double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x > 0 ? x:-x);
    ret += (20.0 * sin(6.0 * x * pi) + 20.0 *sin(2.0 * x * pi)) * 2.0 / 3.0;
    ret += (20.0 * sin(y * pi) + 40.0 * sin(y / 3.0 * pi)) * 2.0 / 3.0;
    ret += (160.0 * sin(y / 12.0 * pi) + 320 * sin(y * pi / 30.0)) * 2.0 / 3.0;
    return ret;
}

double transformLon(double x, double y)
{
    double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x > 0 ? x:-x);
    ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
    ret += (20.0 * sin(x * pi) + 40.0 * sin(x / 3.0 * pi)) * 2.0 / 3.0;
    ret += (150.0 * sin(x / 12.0 * pi) + 300.0 * sin(x / 30.0 * pi)) * 2.0 / 3.0;
    return ret;
}

CLLocationCoordinate2D transformFromWGSToGCJ(CLLocationCoordinate2D wgLoc)
{
    CLLocationCoordinate2D mgLoc;
    if (outOfChina(wgLoc.latitude, wgLoc.longitude))
    {
        mgLoc = wgLoc;
        return mgLoc;
    }
    double dLat = transformLat(wgLoc.longitude - 105.0, wgLoc.latitude - 35.0);
    double dLon = transformLon(wgLoc.longitude - 105.0, wgLoc.latitude - 35.0);
    double radLat = wgLoc.latitude / 180.0 * pi;
    double magic = sin(radLat);
    magic = 1 - ee * magic * magic;
    double sqrtMagic = sqrt(magic);
    dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
    dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * pi);
    mgLoc.latitude = wgLoc.latitude + dLat;
    mgLoc.longitude = wgLoc.longitude + dLon;
    
    return mgLoc;
}

CLLocationCoordinate2D transformFromGCJToWGS(CLLocationCoordinate2D gcLoc)
{
    CLLocationCoordinate2D wgLoc = gcLoc;
    CLLocationCoordinate2D currGcLoc, dLoc;
    while (1) {
        currGcLoc = transformFromWGSToGCJ(wgLoc);
        dLoc.latitude = gcLoc.latitude - currGcLoc.latitude;
        dLoc.longitude = gcLoc.longitude - currGcLoc.longitude;
        if (fabs(dLoc.latitude) < 1e-7 && fabs(dLoc.longitude) < 1e-7) {  // 1e-7 ~ centimeter level accuracy
            // Result of experiment:
            //   Most of the time 2 iterations would be enough for an 1e-8 accuracy (milimeter level).
            //
            return wgLoc;
        }
        wgLoc.latitude += dLoc.latitude;
        wgLoc.longitude += dLoc.longitude;
    }
    
    return wgLoc;
}

///
///  Transform GCJ-02 to BD-09
///
const double x_pi = 3.14159265358979324 * 3000.0 / 180.0;
CLLocationCoordinate2D bd_encrypt(CLLocationCoordinate2D gcLoc)
{
    double x = gcLoc.longitude, y = gcLoc.latitude;
    double z = sqrt(x * x + y * y) + 0.00002 * sin(y * x_pi);
    double theta = atan2(y, x) + 0.000003 * cos(x * x_pi);
    return LocationMake(z * cos(theta) + 0.0065, z * sin(theta) + 0.006);
}

///
///  Transform BD-09 to GCJ-02
///
CLLocationCoordinate2D bd_decrypt(CLLocationCoordinate2D bdLoc)
{
    double x = bdLoc.longitude - 0.0065, y = bdLoc.latitude - 0.006;
    double z = sqrt(x * x + y * y) - 0.00002 * sin(y * x_pi);
    double theta = atan2(y, x) - 0.000003 * cos(x * x_pi);
    return LocationMake(z * cos(theta), z * sin(theta));
}
